﻿using XDCCManager.Model.Servers;
using System.Net;
using System.Threading;
using System.Net.Sockets;
using System;
using System.Net.Security;
using System.Windows.Threading;

namespace XDCCManager.Network.IRC
{
	public sealed class ServerConnection : IServerConnection
	{
		static ServerConnection()
		{
			Instance = new ServerConnection();
			numericSuffix = new System.Text.RegularExpressions.Regex("(\\d+)$", System.Text.RegularExpressions.RegexOptions.Compiled);
		}

		private ServerConnection()
		{
			writeMessageDelegate = new Action<string>(WriteMessage);
			readBuffer = new byte[BufferSize];
			readTokenizer = new LineTokenizer(BufferSize << 1);
			writeBuffer = new System.Text.StringBuilder(3000, 3000);
			ConnectionEncoding = System.Text.Encoding.GetEncoding("ISO-8859-1");
		}

		const int ResolutionWaitTime = 0000;
		const int ConnectionWaitTime = 4000;
		const int FailWaitTime = 1000;
		const int SuccessWaitTime = 500;
		const int BufferSize = 1024;

		private static readonly System.Text.RegularExpressions.Regex numericSuffix;

		private Server server;
		private System.IO.Stream serverStream;
		private bool cancelConnect;
		private bool disconnectRequested;
		private Dispatcher writeDispatcher;
		private Dispatcher readDispatcher;
		private byte[] readBuffer;
		private LineTokenizer readTokenizer;
		private System.Text.StringBuilder writeBuffer;
		private Action<string> writeMessageDelegate;

		public bool Connected { get; private set; }

		public bool Connecting { get; private set; }

		public System.Text.Encoding ConnectionEncoding { get; set; }

		public static ServerConnection Instance { get; private set; }

		public Server Server
		{
			get
			{
				return server;
			}
			set
			{
				if (Connected || Connecting)
					throw new InvalidOperationException("The connection must be closed to change server.");
				server = value;
			}
		}

		public void Connect()
		{
			if (Connected || Connecting)
				throw new InvalidOperationException("The connection is in progress or established.");
			if (Server == null)
				throw new InvalidOperationException("No server specified.");
			if (Server.Addresses.Count == 0)
				throw new InvalidOperationException("No server address available.");
			if ((Server.BotName.Length == 0) || (Server.Nick.Length == 0) || (Server.Password.Length == 0))
				throw new InvalidOperationException("Server definition is not complete.");
			disconnectRequested = false;
			cancelConnect = false;
			Connecting = true;
			OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.Idle));
			ConnectionAttemptState state = new ConnectionAttemptState() { ServerAddressIndex = 0, ServerAddress = Server.Addresses[0] };
			System.ComponentModel.BackgroundWorker worker = new System.ComponentModel.BackgroundWorker();
			worker.DoWork += new System.ComponentModel.DoWorkEventHandler((sender, args) =>
				{
					Thread.Sleep(SuccessWaitTime);
					ResolveAddress(state);
				});
			worker.RunWorkerAsync();
		}

		public void CancelConnect()
		{
			if (Connecting)
			{
				lock (this)
				{
					cancelConnect = true;
				}
			}
		}

		public void Disconnect()
		{
			if (!Connected || Connecting)
				throw new InvalidOperationException("No connection is established");
			lock (this)
			{
				disconnectRequested = true;
				ChatConnection.Instance.Close();
				if (writeDispatcher != null)
				{
					writeDispatcher.BeginInvoke(writeMessageDelegate, DispatcherPriority.Send, IRCEngine.Message.BuildQuitMessage("Disconnect"));
					writeDispatcher.BeginInvoke(new Action(() => WriteToStream()), DispatcherPriority.Send).Wait(TimeSpan.FromSeconds(10));
				}
				CloseStreamAndDispatchers();
			}
			Connected = false;
		}

		#region Connection establishment
		private void ResolveAddress(ConnectionAttemptState state)
		{
			state.AddressIndex = -1;
			state.Addresses = null;
			OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.AddressResolving, state.ServerAddress));
			if (IsCanceled())
				return;
			bool moveToNext = true;
			try
			{
				Dns.BeginGetHostAddresses(state.ServerAddress.Hostname, ResolveAddressCallback, state);
				moveToNext = false;
			}
			catch (SocketException e)
			{
				if (Server.Addresses.Count == 1)
				{
					OnConnectionFailure("Unable to resolve the server address.\nMessage is " + e.Message);
					Connecting = false;
					return;
				}
			}
			catch (ArgumentException e)
			{
				if (Server.Addresses.Count == 1)
				{
					OnConnectionFailure("The specified server address is invalid.\nMessage is " + e.Message);
					Connecting = false;
					return;
				}
			}
			if (moveToNext)
			{
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.AddressResolutionFail, state.ServerAddress));
				Thread.Sleep(FailWaitTime);
				ResolveNextAddress(state);
			}
		}

		private void ResolveNextAddress(ConnectionAttemptState state)
		{
			if (IsCanceled())
				return;
			state.ServerAddressIndex++;
			if (state.ServerAddressIndex >= Server.Addresses.Count)
				state.ServerAddressIndex = 0;
			state.ServerAddress = Server.Addresses[state.ServerAddressIndex];
			Thread.Sleep(ResolutionWaitTime);
			ResolveAddress(state);
		}

		private void ResolveAddressCallback(IAsyncResult result)
		{
			ConnectionAttemptState state = result.AsyncState as ConnectionAttemptState;
			IPAddress[] addresses = Dns.EndGetHostAddresses(result);
			if (IsCanceled())
				return;
			if ((addresses.Length == 0) && (Server.Addresses.Count == 1))
			{
				OnConnectionFailure("Could not resolve the only server address to an IP address.");
				Connecting = false;
				return;
			}
			OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.AddressResolved, state.ServerAddress, addresses));
			state.Addresses = addresses;
			state.AddressIndex = 0;
			Thread.Sleep(SuccessWaitTime);
			Connect(state);
		}

		private void Connect(ConnectionAttemptState state)
		{
			if (state.AddressIndex >= state.Addresses.Length)
			{
				ResolveNextAddress(state);
				return;
			}
			if (IsCanceled())
				return;
			OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.ServerConnecting, state.ServerAddress, state.Addresses[state.AddressIndex]));
			Socket socket = new Socket(state.Addresses[state.AddressIndex].AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			state.Socket = socket;
			try
			{
				socket.BeginConnect(new IPEndPoint(state.Addresses[state.AddressIndex], state.ServerAddress.Port), ConnectCallback, state);
			}
			catch (SocketException e)
			{
				if ((state.Addresses.Length == 1) && (Server.Addresses.Count == 1))
				{
					OnConnectionFailure("An error happend while trying to connect.\nMessage: " + e.Message);
					Connecting = false;
					return;
				}
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.ServerConnectFail, state.ServerAddress, state.Addresses[state.AddressIndex]));
				Thread.Sleep(FailWaitTime);
				ConnectNext(state);
			}
		}

		private void ConnectNext(ConnectionAttemptState state)
		{
			if (IsCanceled())
				return;
			state.AddressIndex++;
			Thread.Sleep(ConnectionWaitTime);
			Connect(state);
		}

		private void ConnectCallback(IAsyncResult result)
		{
			ConnectionAttemptState state = result.AsyncState as ConnectionAttemptState;
			try
			{
				state.Socket.EndConnect(result);
			}
			catch (SocketException e)
			{
				if ((state.Addresses.Length == 1) && (Server.Addresses.Count == 1))
				{
					OnConnectionFailure("An error happend while trying to connect.\nMessage: " + e.Message);
					Connecting = false;
					return;
				}
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.ServerConnectFail, state.ServerAddress, state.Addresses[state.AddressIndex]));
				Thread.Sleep(FailWaitTime);
				ConnectNext(state);
				return;
			}
			if (IsCanceled())
			{
				state.Socket.Close();
				return;
			}
			if (state.ServerAddress.Ssl)
				NegotiateSsl(state);
			else
			{
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.ServerConnected, state.ServerAddress, state.Addresses[state.AddressIndex]));
				serverStream = new NetworkStream(state.Socket, true);
				Thread.Sleep(SuccessWaitTime);
				InitRegistration(state);
			}
		}

		private void NegotiateSsl(ConnectionAttemptState state)
		{
			if (IsCanceled())
			{
				state.Socket.Close();
				return;
			}
			OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.SslNegotiating, state.ServerAddress, state.Addresses[state.AddressIndex]));
			SslStream stream = new SslStream(new NetworkStream(state.Socket, true), false);
			SslAttemptState sslState = new SslAttemptState() { State = state, Stream = stream };
			try
			{
				stream.BeginAuthenticateAsClient(state.ServerAddress.Hostname, null, System.Security.Authentication.SslProtocols.Tls | System.Security.Authentication.SslProtocols.Ssl3, false, NegotiateSslCallback, sslState);
			}
			catch (System.Security.Authentication.AuthenticationException e)
			{
				stream.Close();
				if ((state.Addresses.Length == 1) && (Server.Addresses.Count == 1))
				{
					OnConnectionFailure("An error happend while negotiating SSL.\nMessage: " + e.Message);
					Connecting = false;
					return;
				}
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.SslNegotiationFailed, state.ServerAddress, state.Addresses[state.AddressIndex]));
				Thread.Sleep(FailWaitTime);
				ConnectNext(state);
			}
		}

		private void NegotiateSslCallback(IAsyncResult result)
		{
			SslAttemptState state = result.AsyncState as SslAttemptState;
			try
			{
				state.Stream.EndAuthenticateAsClient(result);
			}
			catch (System.Security.Authentication.AuthenticationException e)
			{
				state.Stream.Close();
				if ((state.State.Addresses.Length == 1) && (Server.Addresses.Count == 1))
				{
					OnConnectionFailure("An error happend while negotiating SSL.\nMessage: " + e.Message);
					Connecting = false;
					return;
				}
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.SslNegotiationFailed, state.State.ServerAddress, state.State.Addresses[state.State.AddressIndex]));
				Thread.Sleep(FailWaitTime);
				ConnectNext(state.State);
				return;
			}
			OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.ServerConnected, state.State.ServerAddress, state.State.Addresses[state.State.AddressIndex]));
			serverStream = state.Stream;
			InitRegistration(state.State);
		}
		#endregion

		#region Connection registration and reading
		private void InitRegistration(ConnectionAttemptState state)
		{
			if (IsCanceled())
			{
				serverStream.Close();
				return;
			}
			writeBuffer.Length = 0;
			(new Thread(new ThreadStart(() =>
			{
				Thread.CurrentThread.IsBackground = true;
				lock (this)
				{
					readDispatcher = Dispatcher.CurrentDispatcher;
				}
				Dispatcher.Run();
			}))).Start();
			(new Thread(new ThreadStart(() =>
			{
				Thread.CurrentThread.IsBackground = true;
				lock (this)
				{
					writeDispatcher = Dispatcher.CurrentDispatcher;
				}
				Dispatcher.CurrentDispatcher.Hooks.DispatcherInactive += WriteHooks_DispatcherInactive;
				Dispatcher.Run();
				Dispatcher.CurrentDispatcher.Hooks.DispatcherInactive -= WriteHooks_DispatcherInactive;
			}))).Start();
			RegistrationState regState = new RegistrationState();
			regState.State = state;
			regState.Step = RegistrationState.RegistrationStep.Start;
			string user = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
			Thread.Sleep(0);
			SendMessage(IRCEngine.Message.BuildNickMessage(Server.Nick));
			SendMessage(IRCEngine.Message.BuildUserMessage(user.Split(' ')[0].ToLower(), 8, user));
			regState.Step = RegistrationState.RegistrationStep.RegistrationSent;
			Read(regState);
		}

		private void Read(RegistrationState regState)
		{
			if (IsCanceled())
			{
				CloseStreamAndDispatchers();
				return;
			}
			try
			{
				serverStream.BeginRead(readBuffer, 0, readBuffer.Length, new AsyncCallback(ReadCallback), regState);
			}
			catch (System.IO.IOException e)
			{
				lock (this)
				{
					if (cancelConnect || disconnectRequested)
						return;
				}
				CloseStreamAndDispatchers();
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.IOError, e));
				Thread.Sleep(FailWaitTime);
				ConnectNext(regState.State);
				return;
			}
			catch (NullReferenceException)
			{
				lock (this)
				{
					if (cancelConnect || disconnectRequested)
						return;
				}
				CloseStreamAndDispatchers();
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.Disconnected));
				Thread.Sleep(FailWaitTime);
				ConnectNext(regState.State);
				return;
			}
		}

		private void CloseStreamAndDispatchers()
		{
			lock (this)
			{
				if (readDispatcher != null)
				{
					readDispatcher.BeginInvokeShutdown(DispatcherPriority.Send);
					readDispatcher = null;
				}
				if (writeDispatcher != null)
				{
					writeDispatcher.BeginInvokeShutdown(DispatcherPriority.Send);
					writeDispatcher = null;
				}
				if (serverStream != null)
				{
					serverStream.Close();
					serverStream = null;
				}
				writeBuffer.Length = 0;
				readTokenizer.Clear();
			}
		}

		private void ReadCallback(IAsyncResult result)
		{
			RegistrationState state = result.AsyncState as RegistrationState;
			int read = 0;
			try
			{
				read = serverStream.EndRead(result);
			}
			catch (System.IO.IOException e)
			{
				lock (this)
				{
					if (cancelConnect || disconnectRequested)
						return;
				}
				CloseStreamAndDispatchers();
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.IOError, e));
				Thread.Sleep(FailWaitTime);
				ConnectNext(state.State);
				return;
			}
			catch (NullReferenceException)
			{
				lock (this)
				{
					if (cancelConnect || disconnectRequested)
						return;
				}
				CloseStreamAndDispatchers();
				OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.Disconnected));
				Thread.Sleep(FailWaitTime);
				ConnectNext(state.State);
				return;
			}
			if (IsCanceled())
			{
				CloseStreamAndDispatchers();
				return;
			}
			ExtractMessages(read, state);
			if (state.Step == RegistrationState.RegistrationStep.Fail)
				return;
			Read(state);
		}

		private void ExtractMessages(int read, RegistrationState state)
		{
			readTokenizer.Append(readBuffer, read);
			string line = readTokenizer.GetLine(ConnectionEncoding);
			while (line != null)
			{
				if (line.Length > 0)
				{
					IRCEngine.Message message = new IRCEngine.Message(line);
					if (ParseMessage(message, state))
						if (MessageReceived != null)
							readDispatcher.BeginInvoke(MessageReceived, this, new MessageEventArgs(message));
					if (state.Step == RegistrationState.RegistrationStep.Fail)
						break;
				}
				line = readTokenizer.GetLine(ConnectionEncoding);
			}
		}

		private bool ParseMessage(IRCEngine.Message message, RegistrationState state)
		{
			if (message.Command.NumericCommand)
			{
				int command = message.Command.CommandValue;
				if (command == IRCEngine.CodesMessages.RPL_WELCOME.Code)
				{
					if (state.Step == RegistrationState.RegistrationStep.RegistrationSent)
					{
						System.Collections.Specialized.StringDictionary @params = IRCEngine.CodesMessages.RPL_WELCOME.getParams(message.Params);
						state.Nick = @params["nick"];
						state.User = @params["user"];
						state.Host = @params["host"];
						state.Step = RegistrationState.RegistrationStep.WelcomeReceived;
						OnConnectionStateUpdate(new ConnectionStateEventArgs(ConnectionStateEventArgs.ConnectionState.WaitingMotd));
					}
				}
				else if (command == IRCEngine.CodesMessages.RPL_ENDOFMOTD.Code)
				{
					if (state.Step == RegistrationState.RegistrationStep.WelcomeReceived)
					{
						state.Step = RegistrationState.RegistrationStep.MotdReceived;
						ChatConnection.Instance.Open(() => {
							if (IsCanceled())
							{
								CloseStreamAndDispatchers();
								ChatConnection.Instance.Close();
								return;
							}
							lock (this)
							{
								Connecting = false;
								Connected = true;
								cancelConnect = false;
							}
							if (ConnectionReady != null)
								ConnectionReady(this, new EventArgs());
						}, (errorMessage) => {
							lock (this)
							{
								Connecting = false;
								cancelConnect = true;
							}
							CloseStreamAndDispatchers();
							ChatConnection.Instance.Close();
							OnConnectionFailure(errorMessage);
						}, OnConnectionStateUpdate);
					}
				}
				else if ((command == IRCEngine.CodesMessages.ERR_NICKNAMEINUSE.Code) ||
					(command == IRCEngine.CodesMessages.ERR_UNAVAILRESOURCE.Code))
				{
					if (state.Nick == null)
						state.Nick = Server.Nick;
					if (!numericSuffix.IsMatch(state.Nick))
						state.Nick = state.Nick + "0";
					System.Text.RegularExpressions.Match match = numericSuffix.Match(state.Nick);
					int suffix = int.Parse(match.Groups[1].Value) + 1;
					state.Nick = state.Nick.Substring(0, match.Index) + suffix;
					SendMessage(IRCEngine.Message.BuildNickMessage(state.Nick));
				}
				else if (command == IRCEngine.CodesMessages.ERR_ERRONEUSNICKNAME.Code)
				{
					CloseStreamAndDispatchers();
					state.Step = RegistrationState.RegistrationStep.Fail;
					Connecting = false;
					OnConnectionFailure("The specified nick is not a valid nick for the server.");
					return false;
				}
			}
			else
			{
				switch (message.Command.CommandName)
				{
					case "PING":
						if (message.Params.Count > 1)
							SendMessage(IRCEngine.Message.BuildPongMessage(message.Params[0], message.Params[1]));
						else
							SendMessage(IRCEngine.Message.BuildPongMessage(message.Params[0]));
						return false;
					case "PRIVMSG":
						{
							string middle = IRCEngine.MessageConverter.LowLevelDecode(message.Params.TrailingParam);
							string client = IRCEngine.MessageConverter.CTCPDecode(IRCEngine.MessageConverter.RemoveCTCPMessages(middle));
							string[] ctcp = IRCEngine.MessageConverter.ExtractCTCPMessages(middle);
							foreach (var request in ctcp)
							{
								string[] tokens = request.Split(' ');
								switch (tokens[0])
								{
									case "FINGER":
										SendMessage(IRCEngine.Message.BuildCTCPReply(message.Prefix.Nick, "FINGER :in the nose."));
										break;
									case "VERSION":
										SendMessage(IRCEngine.Message.BuildCTCPReply(message.Prefix.Nick, "VERSION :not defined."));
										break;
									case "SOURCE":
										SendMessage(IRCEngine.Message.BuildCTCPReply(message.Prefix.Nick, "SOURCE :IYA / DTC"));
										break;
								}
							}
						}
						break;
				}
			}
			return true;
		}
		#endregion

		#region Write to connection
		public void SendMessage(string message)
		{
			lock (this)
			{
				if (writeDispatcher != null)
				{
					writeDispatcher.BeginInvoke(writeMessageDelegate, message);
				}
			}
		}

		private void WriteMessage(string message)
		{
			bool mustWrite = false;
			lock (writeBuffer)
			{
				mustWrite = (writeBuffer.Length + message.Length) >= writeBuffer.Capacity;
			}
			if (mustWrite)
				WriteToStream();
			lock (writeBuffer)
			{
				writeBuffer.Append(message);
			}
		}

		private void WriteToStream()
		{
			try
			{
				lock (writeBuffer)
				{
					if (writeBuffer.Length > 0)
					{
						byte[] content = ConnectionEncoding.GetBytes(writeBuffer.ToString());
						serverStream.Write(content, 0, content.Length);
						writeBuffer.Length = 0;
						serverStream.Flush();
					}
				}
			}
			catch (System.IO.IOException)
			{
			}
			catch (NullReferenceException)
			{
			}
		}

		void WriteHooks_DispatcherInactive(object sender, EventArgs e)
		{
			WriteToStream();
		}
		#endregion

		private bool IsCanceled()
		{
			lock (this)
			{
				if (cancelConnect)
				{
					Connecting = false;
					OnConnectionFailure("Canceled by user.");
					return true;
				}
			}
			return false;
		}

		private void OnConnectionFailure(string message)
		{
			if (ConnectionFailure != null)
				ConnectionFailure(this, new ConnectionErrorEventArgs(message));
		}

		private void OnConnectionStateUpdate(ConnectionStateEventArgs args)
		{
			if (ConnectionStateUpdate != null)
				ConnectionStateUpdate(this, args);
		}

		public event EventHandler<ConnectionErrorEventArgs> ConnectionFailure;
		public event EventHandler<ConnectionStateEventArgs> ConnectionStateUpdate;
		public event EventHandler ConnectionReady;
		public event EventHandler<MessageEventArgs> MessageReceived;
	}
}
